<?php

namespace Fluency;

use Fluency\Metadata\ActionMetadata;

/**
 * @package Fluency
 * @author  Ignas Rudaitis
 * @version 2.0.0
 */
class Dispatcher
{
    private $executor;
    private $filterer;
    private $injector;
    private $renderer;

    public function __construct(Executor $e, Filterer $f, Injector $i, Renderer $r)
    {
        $this->executor = $e;
        $this->filterer = $f;
        $this->injector = $i;
        $this->renderer = $r;
    }

    /**
     * @param Fluency\Request $req
     * @return boolean
     */
    public function canDispatch(Request $req)
    {
        if (!$this->executor->actionExists($req->getAction())) {
            return false;
        }
        $metadata = $this->executor->getActionMetadata($req->getAction());
        return !($req->isExternal() && $metadata->isInternal());
    }

    /**
     * Dispatches the given request and returns the response to it.
     *
     * @throws InvalidArgumentException if it is impossible to dispatch the request
     * @param Fluency\Request $req
     * @return Fluency\Response
     */
    public function dispatch(Request $req)
    {
        $this->ensureRequestCanBeDispatched($req);
        $meta = $this->executor->getActionMetadata($req->getAction());
        try {
            $continuations = $this->applyFilters($meta);
            $result = $this->executeAction($req, $meta);
            $response = $this->renderActionResult($result, $req, $meta);
            return $this->filterer->applyContinuations($continuations, $response);
        } catch (Exceptions\InterruptException $e) {
            return $e->getResponse($this, $req);
        }
    }

    private function ensureRequestCanBeDispatched(Request $req)
    {
        if (!$this->canDispatch($req)) {
            throw new \InvalidArgumentException(
                    "The provided request for action '{$req->getAction()}' cannot be dispatched");
        }
    }

    private function applyFilters(ActionMetadata $meta)
    {
        $names = $meta->getFilters();
        $params = array_map(array($meta, "getFilterParameters"), $names);
        $filters = empty($names) ? array() : array_combine($names, $params);
        return $this->filterer->applyFilters($filters, $this->injector);
    }

    private function executeAction(Request $req, ActionMetadata $meta)
    {
        $injector = $this->injector->withArrayContainer($req->getAllParameters());
        return $this->executor->executeAction($req->getAction(), $injector);
    }

    private function renderActionResult($result, Request $req, ActionMetadata $meta)
    {
        $view = $meta->getView($req->getFormat());
        $params = $meta->getViewParameters($req->getFormat());
        return $this->renderer->render($view, $result + $params);
    }
}
